阿里云MaxCompute半结构化数据思考与创新
1.半结构化数据简析
2.传统方案优劣对比
3.MaxCompute半结构化数据解决方案
4.收益分析分享嘉宾|周宇睿 阿里云 高级技术专家
编辑整理|刘步龙
内容校对|李瑶
出品社区|DataFun
首先来介绍一下什么是半结构化数据。
半结构化数据是相对结构化数据和非结构化数据而言的,所以先来看一下什么是结构化数据和非结构化数据。
结构化数据的概念大家都比较熟悉。传统的关系型数据库是用表的方式对数据进行组织,表的内部定义了字段的数量、类型,以及各种各样字段的属性信息,这些定义本身就包含了丰富的信息。因为在结构化数据的场景中,字段属性被事先严格地定义,所以数据库也好,各种数据引擎、存储引擎都可以通过这些定义获得的信息,有针对性的对数据进行处理。这些处理包括但不限于建立索引,对数据进行排序,对数据进行列存化,也包括向量化执行等等,从而达到一个降低存储成本,提升访问效率的目的。通常来说,结构化数据可以达到非常好的数据读写性能,它的存储效率、压缩效率也可以做得非常好,但是它的灵活性会受到很大的限制。在很多的数据库或者数据仓库当中,如果想改变数据表的结构,通常来说是一个非常高风险的操作,可能会带来很多额外的数据管理成本。
相比之下,非结构化数据本身也是一个很容易理解的概念,我们日常生活中会接触到的,比如说视频、音频、图片、文章等等,都可以算作是一个比较典型的非结构化数据。非结构化数据里面很一个很重要的特征就是没有一个清晰的统一的协议对这些数据内部的结构进行约束,数据本身的内容也没有统一的规律。非结构化数据具备的优势就是几乎是无限的数据灵活性。当然这种无限的灵活性本身也是会有代价的,就是由于没有办法事先对数据本身的结构进行解析,因此很在大部分场景里面,数据引擎没有办法对数据结构进行有效的信息提取,所以一般来说非结构化数据的存储效率和访问性能都是比较差的。
相比于传统的结构化数据,半结构化数据并没有受到来自外部的、来自数据仓库或者数据库的这种表级别的强约束。因此一般来说,半结构化数据会更加的灵活,它可以更好地根据具体的用户场景,用户需求进行动态的变化。也正是因为这个特征,通常来说半结构化数据会有多层嵌套的结构,比如说 Json,Xml,都是很典型的半结构化数据。从非结构化数据到半结构化数据,最后再到结构化数据,数据的灵活性是在不断下降的,但是数据的存储效率、访问性能也在不断地提升。半结构化数据在某种程度上可以说是兼顾了两种类型的优点,一方面它具有比较好的灵活性,另外一方面通过协议本身的结构化的约束,也为高效率的访问和解析提供了帮助。
半结构化数据是一种通用的数据传输和存储结构,被广泛地使用在日志分析、IoT设备的信息采集、移动设备的事件上报,以及自动驾驶等多样化的数据场景中。这也体现了半结构化数据的一个很大的特点,就是它的通用性非常好。同时由于半结构化数据本身的灵活性,它们可以在大部分数据场景下面承载和传递丰富的原始数据的信息。由于其解析协议非常简单,也能够支持我们快速地对数据进行解析和访问。所以一般来说,半结构化数据本身就是一个非常丰富的数据信息的载体。另一方面,比如Json、Xml,具有非常丰富和完善的数据生态,我们可以很方便地在各种语言、各种框架、各种平台上面对这些数据进行解析、生产和消费。多平台的通用性也让半结构化数据这种协议成为了事实上的一种数据的通信标准,可以广泛地接受和使用。从数据仓库处理的角度来说,半结构化数据本身的这种灵活性,也会给上游的业务部门以及中间的数据中台和下游的数据消费决策部门的独立运行,提供一个很好的缓冲。在一个比较大的公司或者团队中,上游是产生数据的业务部门,中间是负责数据处理维护的数据仓库的数据中台部门,还有下游负责决策和消费的决策部门,通常是独立运行的,有着截然不同的运行模式和目标。半结构化数据以其灵活性和易解析的特性可以很好地帮助各个部门进行独立的业务演进,避免由于上游部门频繁的业务迭代带来高昂的跨部门沟通以及数据维护的成本。
在传统的数据仓库中,半结构化数据解决方案分为schema on read和 schema on write两种形式。本质上来讲的就是数据引擎在数据读写的哪一个环节对数据结构进行解析。schema on read,顾名思义就是在数据导入的过程中不对数据做任何解析和处理,直接将数据进行存储,然后只有在数据访问的时候会根据用户具体的请求,依赖引擎的动态解析能力对数据进行解析。由于在数据写入的过程中我们没有对数据本身做出任何约束,因此schema on read的方案一般来说会提供比较好的数据灵活性。和schema on read相反,schema on write方案就是我们在数据写入的时候,就需要根据事先定义好的结构对数据进行解析,将这种半结构化数据的结构转换成传统的结构化数据,然后再导入到数据仓库当中。相比于schema on read,schema on write的灵活性较差,但是能够提供更好的存储和访问性能。
当用户将数据写入到数据仓库中,因为没有对数据进行任何解析,直接是以字符串的方式导入到数据仓库的,因此是以一种行式的数据结构进行存储的。当用户要对这个数据进行查找和解析,比如上图的案例,用户希望统计年纪在18岁以上的用户数量的时候,需要先提取年纪在18岁以上这个特征,由于在数据写入的时候没有提前对数据进行拆分,所以需要对整个数据进行全表扫描,拿到了所有的数据之后进行解压解码,然后获得具体的JSON数据结构,再进一步地根据用户的需求对这个JSON 结构进行处理,最终获得年龄字段。在整个执行链路当中,一方面数据的存储开销非常大,另外一方面整个查询效率由于需要full scan,还需要花费额外的CPU进行解压,同时对这个JSON的数据结构进行解析,所以它的数据解析效率,访问性能都是非常差的。
同样的一个查询请求,在schema on write的场景里面,我们可以只对用户的年龄字段进行读取,然后直接进行数据解压解码,获得完整的数据结构,再进行数据的解析和查询,它的存储效率和查询效率都会更高。
但是 schema on write的方案也不是完美无缺的。一般来说,采用 schema on write方案的时候,会假定上游业务部门不会对这些字段进行频繁的改动,整个数据结构处于一个相对稳定的状态。如果上游的业务部门处于一种快速迭代、快速适应的阶段,那么可能会不断地有增加字段、修改字段的需求。
在上游业务快速迭代的情况下,如果仍然选择使用schema on write 方案,下游的数据中台或者数据维护部门就需要不断地根据上游业务部门的改动,对数表的结构进行适配,不断地、频繁地执行表字段的增加或者修改,这将耗费巨大的业务维护成本。因此我们考虑有没有可能在允许上游业务部门频繁迭代、自由迭代的情况下,既获得比较好的查询效率和比较低的存储成本,又尽可能地降低数据仓库或者数据维护部门的数据维护成本。
这也就是我们提出来的数据仓库半结构化数据场景的一个核心的需求。希望能够同时兼顾数据查询的高性能、数据存储的低成本以及数据演进的灵活性。结合 schema on write和schema on read的优势,一方面在数据写入的过程中进行数据结构的提取和转换,同时也支持对数据读取过程中动态的自适应的访问,从而达到一个降低存储成本和保持灵活性的效果。
MaxCompute是一个适用于数据分析场景的企业级的云服务数仓,以serverless 的架构提供快速的全托管的在线的数据仓库服务。它消除了传统数据平台在扩展性和弹性方面的限制,能够最小化用户运维投入,可以使用户以较低的成本高效地分析和处理海量数据。随着当前数据收集手段的不断丰富,各个行业数据的大量积累,数据规模已经增长到了传统的数据库或者软件行业无法承载的PB甚至EB的级别。MaxCompute提供了离线和流式的数据接入,能够支持超大规模的数据计算和查询加速能力,可以为用户提供包括面向多种计算场景的数据仓库解决方案以及分析建模的服务。MaxCompute还提供完善的数据导入方案和分布式计算的模型。用户不需要关心具体的分布式计算和维护细节,就可以完成大数据分析。通常来说 MaxCompute适用于100GB以上存储规模的计算需求量,最大可以到EB级别。MaxCompute在阿里巴巴集团内部得到了大规模的应用,适用于大型互联网企业的数据仓库和BI分析,网站的日志分析,电子商务场景的交易分析,以及用户特征和兴趣的挖掘。
上图展示了MaxCompute半结构化数据的一个具体场景。用户会将前端的业务数据和业务日志,通过实时或者分批次的方式导入数据仓库,然后与业务数据进行结合。从用户的诉求来看,他们希望能够最大限度地减少入仓过程中数据转换的链路,并提升数据导入的实时性。另一方面数据中台也会对数据进行定时的监控,保证数据质量,同时进行定时的报警触发。在数据的下游,多个不同的业务部门会对数据有不同的解析需求,用户会通过交互式分析、加速查询的方式生成可视化的数据报表。
在该场景中,上游的业务部门和中间的数据中台,还有下游的数据消费决策部门有着各自比较独立的数据需求,在这里半结构化数据就可以成为连接和缓冲各部门不同诉求的一个非常自然的选择。用户在引入半结构化数据的同时,一方面可以允许上游业务根据自身的需求独立演进,快速迭代,中台的数据维护成本也可以降到最低,同时下游各个不同消费部门各自的数据消费需求也可以得到很好的满足。
用户在将半结构化数据导入数据仓库的过程当中,发现在一个相对较短的周期,比如几个小时、几天甚至几周这样一个周期内,用户的数据结构基本上来说是保持稳定的,也就是说在一个较短的时间内,用户的字段的类型和数量是几乎保持不变的。因此在短周期而且空间相邻的数据当中,有机会去提取这些相对稳定的公共数据结构,然后将这些半结构化数据通过列存化的方式来降低存储成本并提升存查询效率。
在长周期的业务迭代中,比如几周甚至几个月的迭代周期里面,业务部门可能会根据具体的业务场景进行相对缓慢平滑的业务迭代,可以通过引擎自身带有的这种动态自适应的能力,去适应和发现长周期当中字段类型或者字段数量的变化,从而达到一个比较综合的半结构化数据的解决方案。
在如上的数据中可以通过对这个数据的扫描提取出来一个所有数据都具备的公共的数据类型,它中间会有四个字段,每个字段会有一个明确的类型,然后通过这个提取的类型,可以将原始的用户数据以及收集到的这个类型,同样地输入给数据转换器,数据转换器就可以将这个数据进行很好的列存化。实现在数据导入数据仓库的过程中,就对数据进行动态的解析,完成数据列存化的过程。
MaxCompute底层采用的是AliORC列存来进行数据的存储。AliORC 是阿里云自己研制的一个高性能的基于开源Apache ORC的数据格式,它能够天然地很好地对这种嵌套结构进行支持,能够在数据结构、数据文件的文件格式层面就很好地去保存不同节点之间的相互映射以及嵌套的信息。当要对某一个比较深的节点进行探查或者裁剪的时候,可以很自然地将这种JSON的路径和这种嵌套结构的节点进行一一映射,然后来做出一个很好的、很自然而高效率的列裁剪。
如上述例子,可以首先通过一个前面提到的schema提取的工作,将它提取成一个具有嵌套特征的数据结构,最终将它转化成一个基于AliORC的列存结构,将每列的数据进行连续的存放,甚至是嵌套类型内部的子节点,也可以把它进行列存化,实现连续存放从而获得更好的压缩性能以及更好的查询性能。
理想情况下用户的所有数据都具有比较好的稳定性和一致性,但是由于半结构化数据本身自有的这种灵活性的特点,很多场景下面脏数据是难以完全避免的。前面的例子假设所有的用户字段提供的数据类型都是非常干净,非常统一的,但在事实的生产环境中,很有可能会由于代码的bug,或是数据传输过程中的错误,导致数据的类型并不完全一致。比如上图中标红的第三个age字段,前面两个JSON数据中age字段都是整型,但是在第三行数据当中,age字段突然变成了一个字符串类型。在这种场景下面,没有办法很好地进行一个统一的公共类型的提取,因为整型和字符串类型很多时候并不是一个互相兼容的类型。在这种情况下,我们会将这种数据保存成一种内部的二进制数据结构,在这种二进制的结构当中,不仅会保存这个字段具体的数据信息,也会保存它的数据类型的信息。在这个例子里面将前两行数据的age字段,同时记录了它的数据类型,也就是它的整型信息,也记录了它的具体的值的信息。然后在第3行数据当中,记录的它是一个string类型,这样就达到了尽可能完整地保存用户数据的目的。因为从平台的角度来说,平台很难判断用户的这个类数据类型的变化到底是出于业务类型的考量、业务自然的演进,还是一个由 bug 导致的错误,所以从数据平台的角度,还是需要能够尽可能完整地保存用户信息。另一方面,这种使用独立的二进制的方式来保存信息的方式也尽可能地保证了数据列存的效率。能够最大限度地保证不同的字段仍然是通过列存的方式进行存储的。在数据访问的时候也尽可能地针对不同字段进行列裁剪。而且某一个列当中出现了脏数据,并不会对其它列的数据类型和数据存储访问效率造成影响。因此最大限度地将脏数据类型和普通数据类型进行了隔离。
另外一种比较棘手的场景就是稀疏数据类型的处理。在一些场景中,每一行用户数据中可能都会存在一些字段,这些字段出现的频率很低。如果使用前面这种公共类型提取的方案,将这些出现频率很低的字段仍然提取为一个独立的列,就会导致底层存储格式上列的数量无限膨胀,列存本身的效率就会变得非常低。因此在数据进行这种类型提取的过程当中,也需要对字段的频率进行统计。对频率较低的字段进行一个统一的归纳处理,将它们放到一个统一的特殊字段当中。在数据访问的过程当中,如果用户查找到了一些在列存化字段当中不存在的列,那么就会在特殊字段当中进行查找。通过这种处理,我们希望能够最终取得一个兼顾效率和灵活性的平衡。
接下来看一看数据引擎是怎么在具体的查询过程中进行自适应的查找的。前面提到,用户数据的结构在一个较长的周期里面是可能会不断地演进和变化的,因此在实际存储的过程中,不同的列存文件,其数据可能会受到用户业务长期演进的影响,因此不同的列存文件实际存储的用户数据的schema可能是不完全一致的。比如前面提到的这个SQL查询的例子,在这个查询过程中,用户要查询年龄这一列,然后将它cast成 int 类型之后,去查询所有大于 18 岁的用户的数量。在这样一个场景中,会先对这个SQL查询进行一个解析,然后把它做成一个 logical plan。在这个plan当中会有多个不同的算子,在最上游的这种聚合算子或者查询算子,它们都是一个强类型的算子,会期望输入的数据是一个整型。但是在底层的table scan读上来的时候,它其实是一个动态数据类型,并没有办法知道这个时候读上来的实际是一个什么类型,因此在中间需要增加一个动态数据转换的能力,将数据读上来的任何类型,通过一种best effort的方式转化成需要的数据类型,再去进行下一步的数据处理。如图中所演示的,会首先对这个数据文件进行列裁剪,读取出来age这个年龄字段。但是大家可以看到,在这个样例当中,不同的数据文件,读出来的age字段的类型是不一样的,有的文件里面可能读出来是字符串类型,有的文件读出来是整型,有的文件读出来是binary 的二进制类型。因此数据引擎就需要根据实际读上来的类型动态判断要不要在中间增加一个数据转换的算子,统一将数据转换成 int 类型,之后再交由上游的filter算子进行数据的处理,从而实现一个自适应的数据处理的能力。
最后来看一下数据查询方面的收益。对比了前面提到的三种方案,一种是完全使用JSON字符串的方式,一种是JSON列存化的方式,另外一种就是原生的列存的方式。大家可以看到无论是数据在做table scan过程中读取的数据量,还是整个数据的查询时间,查询性能都会有接近一个数量级的提升。
相比于原生的列存,Json列存化的方案仍然存在一些提升的空间。在分析之后发现这里面存在的空间主要的原因还是在Json列存化进行数据解析的过程当中,没有办法完全地将日期类型等等很好地转换成对应的原生的列存类型,这也是下一步的工作当中需要改进的方向。
最后对MaxCompute这个半结构化数据的列存化方案进行一个总结。首先,MaxCompute半结构化列存方案是开箱即用的,不需要用户侧做任何改造。它可以最大限度地保证用户只要进行正常的数据导入,就可以享受到半结构化列存方案带来的红利,然后最大限度地降低用户侧改造带来的额外的数据维护成本。另外,通过在写入的时候对数据类型进行动态解析,能够最大限度地去利用数据结构之间的相似性,提取相邻数据之间的公共结构,对数据列存化。同时也对脏数据或者稀疏数据等场景进行了兜底,保证用户在各种场景下的半截刻画数据都可以尽可能地享受到的列存化方案带来的优势。同时通过这种数据的列成化以及数据引擎动态的访问能力,能够最大限度地提升数据的查询效率,达到接近于原生列存的查询效率。最后,通过使用内部的列式存储,能够最大限度地降低存储成本,从而达到降低存储成本的目的。
A:是的,阿里云Max compute在直接提供了一个叫做JSON的数据类型,用户只要设置了这个列的数据类型之后,就可以直接享受到提供了这个列式半结构化、列存化的这么一个带来的这种红利和优势,在用户在导入的过程中,或者用户在数据查询的过程中,都不需要进行任何额外的数据维护和操作。
A:JSON 数据的我们在存储的过程当中并不会直接管理这个JSON内部的数据结构,就是说在数据的源数据存储过程中,并不会直接去理解说这个数据当中到底有多少结构,因为用户的结构可能是非常复杂,也会不断地演进的。因此只是在数据存储的过程中,在文件级别会保留 Json 的这个数据结构。然后会依赖数据结,数据引擎在访问过程中的这么一个动态的能力去对 JSON 的内部结构进行提取。
A:支持的。
分享嘉宾
INTRODUCTION
周宇睿
阿里云
高级技术专家
周宇睿,阿里云高级技术专家,花名闻拙,阿里云MaxCompute存储团队负责人,专注离线存储引擎领域,深耕数据文件格式与编码算法,推动AliORC数据文件格式连续多年业界领先。在持续巩固MC高性能低成本优势的基础上,探索半结构化数据,Transactional table,离线实时一体化等更多样的数仓应用场景,打造以MC为核心的数据生态。
往期优质文章推荐
往期推荐
点个在看你最好看